跳到主要内容

Go 语言的垃圾回收机制

垃圾回收算法

常见的垃圾回收算法有:

引用计数

每个对象维护一个引用计数,如果这个对象被销毁,则计数 -1 ,当计数为 0 时,回收该对象。

  • 优点:对象可以很快被回收,不会出现内存耗尽或到达阀值才回收。
  • 缺点:不能很好的处理循环引用

标记-清除

从根变量开始遍历所有引用的对象,引用的对象标记“被引用”,没有被标记的则进行回收。

  • 优点:解决了引用计数的缺点。
  • 缺点:需要 STW(stop the world),暂时停止程序运行。
提示

垃圾回收需要 STW 是因为在进行垃圾回收期间,必须暂停程序的执行,以确保垃圾回收器可以安全地访问和操作内存中的对象。在停止期间,应用程序的所有线程都被暂停,防止并发访问可能会导致不一致的状态。

停止世界机制的使用有以下原因:

  1. 内存一致性:在进行垃圾回收时,需要确保垃圾回收器能够正确地追踪和更新对象的引用关系。如果允许应用程序继续执行,可能会导致对象引用的变化,从而导致垃圾回收器无法正确地工作。
  2. 安全性:垃圾回收器需要对内存进行重排、对象移动或释放等操作。在执行这些操作时,如果其他线程继续对对象进行读写操作,可能会导致数据不一致或潜在的安全问题。

分代收集

按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。(Java 系列就用这个)

  • 优点:回收性能好
  • 缺点:算法复杂

在分代收集算法中,通常将堆分为新生代(Young Generation)和老年代(Old Generation)。新生代用于存放新创建的对象,而老年代则用于存放存活时间较长的对象。分代收集算法基于一个重要的观察结果,即大部分对象在其创建后很快变得不可达,而只有少部分对象会存活较长时间。

在新生代中,通常采用较轻量级的垃圾回收策略,如复制算法(Copying)或标记-清除算法(Mark and Sweep),这些策略可以在短时间内完成垃圾回收,并且不需要停止程序的执行(无需 STW)。常见的做法是使用两个相互交换的半空间(Semi-space),在垃圾回收过程中将存活的对象从一个半空间复制到另一个半空间,并将原来的半空间一次性清空。

而对于老年代,由于存活对象较多且存活时间较长,常常采用更重量级的垃圾回收策略,如标记-清除、标记-整理(Mark and Compact)或者分区算法(Partitioning),这些策略可能需要暂停程序的执行(需要 STW)才能进行全局的垃圾回收操作。

需要注意的是,具体的分代收集算法和实现细节可能因编程语言、运行环境和垃圾回收器的设计而有所不同。某些情况下,也可能出现特殊的优化技术,使得某些垃圾回收操作不需要完全的 STW,而只需要部分的停顿时间。

Go 语言的 GC(垃圾回收)

Go 的三色标记算法

三色标记算法是在 Go V1.5 开始使用的,三色只是为了叙述上方便抽象出来的一种说法,实际上对象并没有颜色之分。这里的三色,对应了垃圾回收过程中对象的三种状态:

  • 灰色:对象还在标记队列中等待
  • 黑色:对象已被标记,该对象不会在本次GC中被清理
  • 白色:对象未被标记,该对象将会在本次GC中被清理

过程如下所示:

  1. 初始状态下所有对象都是白色的。
  2. 从根节点开始遍历所有对象,把遍历到的对象变成灰色对象(备注:这里变成灰色对象的都是根节点的对象)。
  3. 遍历灰色对象,将灰色对象引用的对象(备注:这里指的是灰色对象引用到的所有对象,包括灰色节点间接引用的那些对象)也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。
  4. 循环步骤3,直到灰色对象全部变黑色。
  5. 通过写屏障(write-barrier)检测对象有变化,重复以上操作(备注:因为 mark 和用户程序是并行的,所以在上一步执行的时候可能会有新的对象分配,写屏障是为了解决这个问题引入的)。
  6. 收集所有白色对象(垃圾)。
提示

在 Go 语言中,"间接引用"(indirect reference)是指通过指针间接引用的对象或值。当我们使用指针来访问或修改某个对象或值时,就可以说我们在进行间接引用。

在 Go 中,可以使用指针类型来声明和操作对象或值的引用。指针是一个特殊的变量,它存储了一个对象或值的内存地址。通过使用指针,我们可以间接地访问和修改指向的对象或值。

下面是一个简单的示例,展示了 Go 中的间接引用:

package main

import "fmt"

func main() {
num := 42 // 声明一个整数变量
ptr := &num // 声明一个指向整数变量的指针

fmt.Println("num =", num) // 直接引用num的值
fmt.Println("ptr =", *ptr) // 通过间接引用访问num的值
fmt.Println("&num =", &num) // 直接引用num的内存地址
fmt.Println("ptr =", ptr) // 直接引用指针ptr的值(即num的内存地址)
}

运行以上代码将输出:

num = 42
ptr = 42
&num = 0xc0000140c8
ptr = 0xc0000140c8

在上面的示例中,我们声明了一个整数变量 num 并赋值为 42。然后,我们声明了一个指针 ptr,它指向整数变量 num 的内存地址。通过在指针前加上 * 符号,我们可以进行间接引用,从而访问和修改指针指向的值。

通过间接引用,我们可以避免直接操作变量本身,而是通过指针来对变量进行操作。这在某些情况下非常有用,例如在函数之间传递大的数据结构或在需要共享和修改数据时。间接引用也是 Go 语言中处理复杂数据结构的重要机制。

根对象是什么?

根对象是垃圾回收算法中的起始点,它们是程序中被直接引用或全局可访问的对象。根对象是垃圾回收器开始遍历和标记的起点,通过跟踪这些根对象及其引用链,可以确定哪些对象是可达的,哪些对象是不可达的,从而进行垃圾回收。

在 Go 语言中,根对象主要包括以下几个部分:

  1. 全局变量:Go 语言中的全局变量是在包级别声明的变量,它们在整个程序执行期间都存在。垃圾回收器会将全局变量作为根对象来开始遍历和标记其他对象。

  2. 栈对象:Go 语言中的函数调用过程中的局部变量和临时数据存储在栈上。垃圾回收器会扫描当前活跃的协程的栈,将栈对象作为根对象来进行垃圾回收。

  3. 其他系统级别的根对象:Go 语言运行时系统会维护一些与系统相关的根对象,例如 goroutine 调度器中的数据结构等。这些根对象也会被垃圾回收器作为根对象来遍历和标记其他对象。

为什么需要灰色对象

灰色对象在三色标记法中的作用是帮助确定需要继续遍历的对象集合。具体来说,灰色对象表示已经被标记为不需要回收的对象,但其引用的其他对象尚未被标记。这些灰色对象的引用链可能指向尚未遍历的其他对象,因此需要进一步探索和标记这些对象。

为什么需要灰色对象呢?主要有两个原因:

  1. 遍历引用链:灰色对象表示需要遍历其引用链以继续标记其他对象。在垃圾回收过程中,从根对象(例如全局变量、堆栈中的变量)出发,通过对象之间的引用关系,逐步遍历对象图。灰色对象的引用链可能指向其他未被标记的对象,因此需要继续遍历并标记这些对象,确保所有可达的对象都被正确标记。
  2. 并发标记:三色标记法是一种并发垃圾回收算法,它允许垃圾回收器在不停止应用程序的情况下进行工作。灰色对象的引入允许垃圾回收器在标记阶段并发地遍历和标记对象图。在并发标记的过程中,灰色对象的引用链可能被其他线程或协程修改,但由于并发标记的机制,可以安全地继续进行遍历和标记。

触发的时机

在 Go 语言中,垃圾回收的触发时机是由运行时系统自动决定的,开发人员无需显式触发垃圾回收。Go 语言的垃圾回收器会在以下情况下自动触发:

  1. 定时触发:Go语言的垃圾回收器会根据经过的时间来触发垃圾回收。具体的时间间隔由运行时系统根据程序的运行状况和垃圾回收的性能目标动态调整。
  2. 内存分配触发:垃圾回收器会根据内存分配的情况来触发垃圾回收。当程序进行大量的内存分配时,垃圾回收器会检测到堆上的内存使用达到一定阈值,然后触发垃圾回收以释放不再使用的对象所占用的内存。
  3. 堆大小触发:垃圾回收器会根据堆的大小来触发垃圾回收。当堆的大小超过一定阈值时,垃圾回收器会被触发,以确保堆的大小在可控范围内。

需要注意的是,Go语言的垃圾回收是在后台并发进行的,它不会停止整个程序的执行。垃圾回收器会在后台运行,并在需要的时候暂停程序的执行,对内存中的对象进行标记、清除等操作。

备注

在 Go 语言中,垃圾回收器的阈值是由运行时系统动态调整的,开发人员无法直接获取或设置。Go 语言的运行时系统会根据程序的运行情况和垃圾回收的性能目标自动调整垃圾回收的阈值。

注意:这里的无法修改阈值,不代表不能修改频率,可以通过 GOGC 来修改垃圾回收的频率。(下面会讲到)

尽管开发人员无法直接知道具体的垃圾回收阈值,但可以通过一些手段来观察和调整垃圾回收的行为。以下是一些方法:

  1. GODEBUG 环境变量:可以使用 GODEBUG 环境变量来调整垃圾回收器的一些参数和输出。通过设置 GODEBUG 环境变量为 gctrace=1,可以启用垃圾回收的跟踪输出,以了解垃圾回收的行为和触发时机。

  2. 运行时统计信息:Go 语言的运行时包(runtime)提供了一些关于垃圾回收的统计信息。可以使用 runtime 包中的函数如 ReadMemStats 来获取当前的内存使用情况、垃圾回收的次数等信息。这些统计信息可以帮助了解垃圾回收的行为和效果。

  3. 性能剖析工具:Go 语言提供了性能剖析工具(pprof),它可以帮助分析程序的性能和资源使用情况。通过使用 pprof 工具进行堆剖析 (Heap Profiling),可以观察堆的使用情况和垃圾回收的效果,从而间接了解垃圾回收的阈值。

需要注意的是,具体的垃圾回收算法和实现细节可能会因Go语言版本而有所不同,而垃圾回收的阈值也会受到运行环境和配置的影响。通常情况下,Go语言的运行时系统会根据程序的运行状况和垃圾回收的性能目标来自动调整垃圾回收的阈值,以达到合适的内存管理和性能平衡。

三色标记的实际例子

1、初识阶段,所以对象均为白色,调用情况为:

2、GC 开始扫描,从根节点开始遍历,发现只有 A 和 F 是根节点,于是将 A、F 从变为灰色对象。

3、GC 继续扫描灰色对象,会将灰色对象的节点中引用的节点也变为灰色对象,A 节点引用的节点 B、C、D 会被变为灰色对象,接着 A 的所有子节点遍历完毕,便会变为黑色对象,而 F 节点没有子节点,也会变为黑色对象。

4、GC 会循环遍历灰色对象,直到灰色对象之中没有节点为止,在本例中,发现 B、C、D 都没有子节点是白色,便将 B、C、D 都变为黑色对象。

5、剩下 E、G、H 为白色对象,GC 便进行回收这些白色对象。

6、上面的垃圾回收结束之后,GC 会在进行一步操作,也就是将黑色对象重新变色成白色对象,供下一次垃圾回收使用。

垃圾回收优化

上面讲清除-标记算法的时候讲过,这个算法的缺点就是垃圾回收需要 STW,这导致 GC 的效率会下降,所以 Golang 需要对这块进行优化

写屏障(Write Barrier)

在Go语言的垃圾回收器中,写屏障(Write Barrier)是一种用于维护对象引用关系的机制,它在对象被写入时进行拦截和处理,以确保垃圾回收器能够正确地跟踪对象之间的引用关系。

写屏障的主要目的是在对象被修改时,将相关的引用关系传递给垃圾回收器,以确保被修改对象和引用它的对象都被正确地标记和保留。

在使用三色标记法进行垃圾回收时,写屏障的作用是在写操作发生时,确保对象的颜色变化得到正确处理。具体来说,当将一个灰色对象标记为黑色时,如果发现该对象引用了一个白色对象,那么写屏障会将这个白色对象标记为灰色,以确保对象图的完整性和正确性。

写屏障的实现可以分为两个步骤:

  1. 拦截写入操作:当程序将一个引用写入对象的字段时,写屏障会拦截该写入操作。它会检查写入的引用是否指向一个堆上的对象,如果是,则继续执行下一步骤;否则,直接进行写入操作,因为堆外对象不需要进行垃圾回收。

  2. 处理引用关系:在写屏障确定写入的引用指向堆上的对象后,它会执行特定的操作来维护对象之间的引用关系。一种常见的处理方式是将该引用添加到一个待处理的队列中,垃圾回收器在适当的时机会扫描这个队列,并根据新的引用关系更新对象的标记状态。

写屏障的引入能够减少垃圾回收器扫描和标记的开销,因为它只在对象被写入时进行拦截和处理,而不是对整个堆进行扫描。写屏障的存在使得垃圾回收器能够更精确地追踪对象之间的引用关系,避免因为写入操作而导致的引用丢失或错误。

辅助GC(Mutator Assist)

在 Go 语言的垃圾回收器中,辅助 GC(Mutator Assist)是一种机制,用于在程序运行过程中帮助垃圾回收器更高效地完成垃圾回收操作。

垃圾回收器在进行垃圾回收时,需要停止程序的执行(Stop The World,简称STW)并扫描堆上的对象。这个过程会导致程序的暂停,影响程序的响应性能和实时性。为了减少暂停时间,Go语言的垃圾回收器引入了辅助GC机制。

辅助 GC 机制的核心思想是让程序的执行线程(mutator)在垃圾回收过程中协助进行一部分的垃圾回收工作,从而分担垃圾回收器的负担。具体来说,当垃圾回收器开始执行垃圾回收时,它会请求执行线程暂时停止执行,并协助完成一些与垃圾回收相关的任务。

辅助 GC 的任务包括但不限于以下几个方面:

  1. 根扫描:执行线程可以帮助垃圾回收器扫描根对象,标记所有可达的对象。这样可以加快垃圾回收的速度,减少STW时间。

  2. 活跃对象识别:执行线程可以协助垃圾回收器识别活跃对象,即那些正在被执行线程使用的对象。这可以帮助垃圾回收器更准确地判断对象的生存状态,避免错误地回收正在使用的对象。

  3. 写屏障:执行线程可以协助执行写屏障操作,即在对象被写入时更新相关的引用关系,以保持垃圾回收器的引用追踪准确。

辅助 GC 机制通过将垃圾回收的一部分工作分摊给执行线程,可以减少 STW 时间,提高程序的响应性能。同时,它也带来了一定的开销,因为执行线程需要在协助垃圾回收时暂停程序的执行。

实际开发的时候如何优化?

在实际开发中,可以采取一些策略来优化 Golang 的垃圾回收(GC),以提高应用程序的性能和响应能力。以下是一些常见的优化技巧:

1、减少内存分配:频繁的内存分配会增加垃圾回收的压力。通过复用对象、使用对象池或缓冲池等技术,可以减少内存分配的次数,降低垃圾回收的频率。

2、避免短暂对象:短暂对象是指生命周期很短的对象,它们往往会在下一次垃圾回收之前被回收。尽量避免创建过多的短暂对象,可以减少垃圾回收的负担。

3、限制并发量:垃圾回收是一个需要占用 CPU 资源的过程。当并发量过高时,可能导致垃圾回收无法及时完成,从而影响应用程序的性能。可以通过限制并发量或调整垃圾回收的阈值来平衡并发度和垃圾回收的负载。

备注

CPU 资源竞争:垃圾回收是一个需要占用 CPU 资源的过程。当并发量过高时,多个并发任务会争夺 CPU 的执行时间,导致垃圾回收无法充分利用 CPU 资源,从而延迟垃圾回收的执行。

内存分配速度超过回收速度:高并发情况下,应用程序可能会频繁地进行内存分配。如果内存分配的速度超过了垃圾回收的速度,那么垃圾回收可能无法及时回收已分配的内存,导致内存使用量持续增长。

垃圾回收器的竞争:Golang 的垃圾回收器是并发执行的,它会与应用程序的其他并发任务竞争 CPU 资源。当并发量过高时,垃圾回收器可能无法获得足够的 CPU 时间来执行垃圾回收,从而导致垃圾回收无法及时完成。

垃圾回收开销增加:高并发情况下,垃圾回收的开销可能会增加。垃圾回收器需要扫描和标记所有存活的对象,并清理未使用的内存。当并发量过高时,垃圾回收器需要处理更多的对象,增加了回收的开销,导致垃圾回收的执行时间延长。

4、调整垃圾回收参数:Golang 提供了一些与垃圾回收相关的环境变量和运行时参数,可以根据应用程序的需求进行调整。例如,可以调整 GC 的时间阈值、并发度、内存占用限制等参数,以优化垃圾回收的性能和效果。

5、使用高效的数据结构:选择适当的数据结构和算法,可以减少内存的占用和垃圾回收的压力。例如,使用切片代替动态数组、使用字节缓冲区代替字符串拼接等,都可以减少内存分配和垃圾回收的开销。

下面是一个示例,演示了如何使用切片(Slice)代替动态数组,从而减少内存分配和垃圾回收的开销。

package main

import (
"fmt"
)

type Item struct {
ID int
Name string
// 其他字段...
}

func processItems(items []Item) {
// 处理 items
for _, item := range items {
fmt.Println(item)
}
}

func main() {
items := make([]Item, 0, 1000000)

// 模拟添加大量数据到切片
for i := 0; i < 1000000; i++ {
item := Item{
ID: i,
Name: fmt.Sprintf("Item %d", i),
}
items = append(items, item)
}

// 处理数据
processItems(items)
}

在上述示例中,我们使用了切片(Slice)来存储一组数据项(Item)。通过预先指定切片的容量,即 make([]Item, 0, 1000000),我们为切片分配了足够的空间来存储100万个数据项。这样,切片的容量在添加数据时不需要频繁地扩容,减少了内存分配的次数。

相比于使用动态数组(使用 append 函数动态添加元素),使用切片的好处是,当切片的容量不足时,切片会自动扩容。扩容操作会重新分配更大的内存块,并将原有数据复制到新的内存块中。这种扩容操作会导致额外的内存分配和垃圾回收的开销。

通过使用切片来存储数据,我们可以减少内存分配的次数,从而减少了垃圾回收的压力。切片会随着数据的增加而自动扩容,但在扩容时会尽量复用已有的内存块,减少了垃圾回收的工作量。

6、分析和优化内存使用:通过使用内存分析工具,如 pprof 和 Go 标准库中的 runtime 包,可以检测内存泄漏、查找内存使用过多的地方,并进行相应的优化。

7、使用第三方库和框架:有些第三方库和框架提供了专门针对 GC 优化的功能和技巧。例如,sync.Pool 可用于对象池管理,grpc 框架提供了连接池和流控制机制等。

逃逸是怎么发生的?

这里先来看下 Go 的逃逸分析机制,在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。

Go 语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆还栈上。

Go 语言逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。简单来说,编译器会分析代码的特征和代码生命周期,Go 中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。

编译器会根据变量是否被外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中;
  2. 如果函数外部存在引用,则必定放到堆中;

以下举个逃逸的例子:

func foo() *int {
t := 3
return &t
}

这里 t 是局部变量所以正常情况是放在栈中的,但是这里返回了这个变量的指针,所以发生了逃逸

实际开发中如何优化?

在Go语言中,可以采取以下几种方法来避免频繁的垃圾回收(GC):

  1. 使用对象池:通过使用对象池(Object Pool)技术,可以重复利用对象,减少对象的创建和销毁。对象池维护一定数量的对象实例,程序需要使用对象时,从对象池中获取,并在使用完毕后归还给对象池。这样可以减少频繁的对象分配和回收,降低 GC 的负担。

  2. 避免产生大量临时对象:尽量避免在循环或频繁调用的函数中产生大量临时对象。例如,避免字符串拼接时频繁创建新的字符串对象,可以使用 strings.Builderbytes.Buffer 进行高效的字符串拼接。

  3. 限制对象的作用域:尽量将对象的作用域限制在需要的范围内,及时释放不再使用的对象。这样可以让垃圾回收器更早地回收无用对象,减少堆上的压力。(所以避免逃逸)

  4. 减少内存分配:尽量减少内存的分配,特别是较大对象的分配。可以通过预分配、对象复用等方式来减少内存分配的次数。同时,注意避免使用过大的数据结构,合理设计数据结构和算法,以降低内存的占用。

  5. 调整垃圾回收器的参数:Go 语言提供了一些环境变量和运行时参数,可以对垃圾回收器的行为进行调优。例如,可以调整 GOGC 环境变量来设置 GC 的触发阈值,调整 GODEBUG 环境变量来输出垃圾回收器的调试信息。根据实际情况进行合理的参数设置,以达到更好的性能和 GC 效果。

  6. 并发优化:使用 Go 语言提供的并发机制,如 goroutine 和 channel,可以将任务分解为并发执行的小块,减少每个块的内存占用,减轻 GC 的压力。合理地使用并发可以提高程序的吞吐量和并发度,减少单个 GC 操作的影响。

GOGC 调整 GC 的频率

GOGC 是一个环境变量,用于设置Go语言的垃圾回收(GC)触发阈值。它影响着在何时启动垃圾回收操作。

GOGC 的值是一个百分比,表示垃圾回收开始前,堆中已分配的对象占可用空间的百分比。默认情况下,GOGC 的值为 100,即当堆中已分配对象占可用空间的百分比达到 100% 时,将触发垃圾回收。

通过调整 GOGC 的值,可以改变垃圾回收的触发时机。较小的 GOGC 值会使垃圾回收更频繁地发生,而较大的 GOGC 值则会减少垃圾回收的频率。当 GOGC 的值为 0 时,禁用自动垃圾回收。

例如,可以通过设置环境变量 GOGC=50 来将 GOGC 的值设为 50,表示当堆中已分配对象占可用空间的百分比达到 50% 时,触发垃圾回收。

调整 GOGC 的值需要根据具体应用场景和内存使用情况进行合理的权衡和测试。较小的 GOGC 值可能会增加垃圾回收的频率,但也可能导致程序更频繁地暂停执行(STW)以进行垃圾回收操作。较大的 GOGC 值可能会减少垃圾回收的频率,但也可能导致堆中对象的累积增长,增加每次垃圾回收的工作量和 STW 时间。

注意,GOGC只是垃圾回收的触发阈值之一,实际的垃圾回收行为还受到其他因素的影响,如堆大小、CPU利用率等。因此,在调优和优化时,还需综合考虑其他相关参数和因素。

调整 GOGC 的例子

当需要调整 GOGC 的值时,可以考虑根据实际场景和性能需求进行调试和测试。下面是一个调整 GOGC 的例子:

假设有一个运行在生产环境中的 Go 应用程序,需要提高其性能并减少垃圾回收的频率。当前的 GOGC 值为默认值 100,即当堆中已分配对象占可用空间的百分比达到 100% 时触发垃圾回收。我们希望减少垃圾回收的频率,同时在性能和内存占用之间达到一个平衡。

  1. 开始测试:在生产环境中进行性能测试,记录应用程序的吞吐量、响应时间和内存占用等指标。
  2. 改变 GOGC 值:通过设置环境变量来改变 GOGC 的值。可以尝试将 GOGC 的值设置为较小的值,如 50 或 30,以增加垃圾回收的频率。
  3. 重新运行测试:重新运行性能测试,记录相同的指标,比较与默认设置下的性能差异。
  4. 分析结果:根据测试结果分析性能和内存占用的变化。观察应用程序的吞吐量、响应时间、垃圾回收的频率和 STW 时间等指标的变化情况。
  5. 进一步调优:根据测试结果,根据实际需求进行进一步调优。如果减小 GOGC 的值提高了性能但导致更频繁的垃圾回收,可以尝试逐步增加 GOGC 的值,找到一个平衡点。如果增加 GOGC 的值减少了垃圾回收但影响了性能,可以考虑将 GOGC 的值调整到一个合适的范围。

通过多次测试和调整,找到一个适合应用程序的 GOGC 值,以达到性能和垃圾回收的平衡。需要注意的是,每个应用程序的特性和环境不同,因此最佳的 GOGC 值可能会因应用程序而异。所以,建议根据具体情况进行实际测试和调优。

小对象多了会造成 GC 压力?

  1. 内存分配:每当创建一个新的对象时,Go 语言的运行时系统会将其分配到堆上。对象的创建和销毁是常见的操作,当存在大量小对象时,频繁的内存分配和释放会导致 GC 频繁地进行回收操作。

  2. 堆空间:Go 语言的堆空间是用于动态分配内存的区域。堆空间的大小是有限的,当小对象的数量增加时,堆空间可能很快被填满。当堆空间不足时,GC 会触发并尝试回收不再使用的对象,以便为新对象腾出空间。频繁的 GC 操作会导致系统的延迟增加。

  3. 垃圾回收算法:Go语言的垃圾回收器使用了基于标记-清除(mark-and-sweep)算法(三色标记)的垃圾回收策略。该算法需要遍历堆上的对象,并标记出活动对象和垃圾对象。当有大量小对象存在时,垃圾回收器需要遍历更多的对象,增加了 GC 的负担和时间消耗。

Reference

Go语言的逃逸分析